﻿// Copyright © Oleg V. Polikarpotchkin 2008
// <copyright file="MainWindow.xaml.cs" company="Oleg V. Polikarpotchkin">
// Copyright © 2008 Oleg V. Polikarpotchkin. All Right Reserved
// </copyright>
// <author>Oleg V. Polikarpotchkin</author>
// <email>ov-p@yandex.ru</email>
// <date>2008-12-23</date>
// <summary>OpenWPFChart library.
// Temperature Chart Sample. Chart is composed in MainWindow.
// It has three axis - one X-axis for dates and two Y-axes for temperature in Celsius and Fahrenheit.
// </summary>
// <revision>$Id$</revision>

using System;
using System.Collections; // for ArrayList
using System.Collections.Generic;
using System.Collections.ObjectModel; // For ObservableCollection
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using Microsoft.Win32; // For OpenFileDialog and SaveFileDialog
using OpenWPFChart.Helpers;
using OpenWPFChart.Parts;

namespace TemperatureSample
{
	/// <summary>
	/// OpenWPFChart library Temperature Chart Sample main Window.
	/// </summary>
	/// <remarks>
	/// Chart is composed in MainWindow. It has three axis - one X-axis for dates and 
	/// two Y-axes for temperature in Celsius and Fahrenheit.
	/// </remarks>
	public partial class MainWindow : Window
	{
		/// <summary>
		/// Initializes a new instance of the <see cref="MainWindow"/> class.
		/// </summary>
		public MainWindow()
		{
			InitializeComponent();
		}

		/// <summary>
		/// Raises the <see cref="E:System.Windows.FrameworkElement.Initialized"/> event. This method is invoked whenever <see cref="P:System.Windows.FrameworkElement.IsInitialized"/> is set to true internally.
		/// </summary>
		/// <param name="e">The <see cref="T:System.Windows.RoutedEventArgs"/> that contains the event data.</param>
		protected override void OnInitialized(EventArgs e)
		{
			base.OnInitialized(e);

			// Load MRU files list
			ArrayList mru = Properties.Settings.Default.MRU;
			if (mru != null)
			{
				foreach (string fileName in mru)
				{
					MenuItem item = new MenuItem() { Header = fileName };
					item.Click += mru_Click;
					mnuRecentFiles.Items.Add(item);
				}
			}
		}

		/// <summary>
		/// Saves the settings.
		/// Raises the <see cref="E:System.Windows.Window.Closed"/> event.
		/// </summary>
		/// <param name="e">An <see cref="T:System.EventArgs"/> that contains the event data.</param>
		protected override void OnClosed(EventArgs e)
		{
			base.OnClosed(e);

			// MRU files list.
			ArrayList mru = null;
			if (mnuRecentFiles.Items.Count > 0)
			{
				mru = new ArrayList();
				foreach (MenuItem item in mnuRecentFiles.Items)
				{
					mru.Add(item.Header as string);
				}
			}
			Properties.Settings.Default.MRU = mru;

			Properties.Settings.Default.Save();
		}

		#region Dependency Properties
		#region HorizontalScale
		/// <summary>
		/// Identifies the HorizontalScale dependency property.
		/// </summary>
		public static readonly DependencyProperty HorizontalScaleProperty
			= DependencyProperty.Register("HorizontalScale", typeof(ChartScale), typeof(MainWindow)
				, new FrameworkPropertyMetadata(null
					, FrameworkPropertyMetadataOptions.AffectsMeasure
						| FrameworkPropertyMetadataOptions.AffectsRender
					)
				);
		/// <summary>
		/// Gets or sets the HorizontalScale property.
		/// </summary>
		/// <value/>
		public ChartScale HorizontalScale
		{
			get { return (ChartScale)GetValue(HorizontalScaleProperty); }
			set { SetValue(HorizontalScaleProperty, value); }
		}
		#endregion HorizontalScale

		#region VerticalScale
		/// <summary>
		/// Identifies the VerticalScale dependency property.
		/// </summary>
		public static readonly DependencyProperty VerticalScaleProperty
			= DependencyProperty.Register("VerticalScale", typeof(ChartScale), typeof(MainWindow)
				, new FrameworkPropertyMetadata(null
					, FrameworkPropertyMetadataOptions.AffectsMeasure
						| FrameworkPropertyMetadataOptions.AffectsRender
					)
				);
		/// <summary>
		/// Gets or sets the VerticalScale property.
		/// </summary>
		/// <value/>
		public ChartScale VerticalScale
		{
			get { return (ChartScale)GetValue(VerticalScaleProperty); }
			set { SetValue(VerticalScaleProperty, value); }
		}
		#endregion VerticalScale
		#endregion Dependency Properties

		#region Chart Events Handlers
		/// <summary>
		/// Handles the SelectionChanged event of the chart control.
		/// </summary>
		/// <param name="sender">The source of the event.</param>
		/// <param name="e">The <see cref="System.Windows.Controls.SelectionChangedEventArgs"/> instance containing the event data.</param>
		private void chart_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
		{
			Trace.WriteLine(string.Format("***** MainWindow.chart_SelectionChanged Source={0} OriginalSource={0}", e.Source, e.OriginalSource));
		}

		/// <summary>
		/// Handles the MouseEnterItem event of the OpenWPFChart.Item element.
		/// </summary>
		/// <param name="sender">The source of the event.</param>
		/// <param name="e">The <see cref="OpenWPFChart.MouseItemEventArgs"/> instance containing the event data.</param>
		private void item_Enter(object sender, MouseItemEventArgs e)
		{
			Cursor = Cursors.Hand;
		}

		/// <summary>
		/// Handles the MouseLeaveItem event of the OpenWPFChart.Item element.
		/// </summary>
		/// <param name="sender">The source of the event.</param>
		/// <param name="e">The <see cref="OpenWPFChart.MouseItemEventArgs"/> instance containing the event data.</param>
		private void item_Leave(object sender, MouseItemEventArgs e)
		{
			Cursor = null;
		}
		#endregion Chart Events Handlers

		#region Load Data
		/// <summary>
		/// Loads CurveData from the file.
		/// </summary>
		/// <param name="fileName">Name of the file.</param>
		/// <exception cref="Exception">Misc IO and file parser exceptions.</exception>
		private bool loadCurveData(string fileName)
		{
			ObservableCollection<ItemData> dataCollection = null;
			FileInfo fi = new FileInfo(fileName);
			try
			{
				if (fi.Extension.ToLower() == ".txt")
					dataCollection = SpaceSeparatedDataFileParser.Parse(fileName);
				else if (fi.Extension.ToLower() == ".xml")
					dataCollection = XMLDataFileParser.Parse(fileName);

				if (dataCollection == null || dataCollection.Count == 0)
				{
					MessageBox.Show("Data wasn't loaded");
					return false;
				}
			}
			catch (Exception ex)
			{
				MessageBox.Show(ex.Message);
				return false;
			}

			// Chart area size.
			Size areaSize = new Size(chart.ActualWidth > 0 ? chart.ActualWidth : ActualWidth
				, chart.ActualHeight > 0 ? chart.ActualHeight : ActualHeight);

			// Set the Chart control X,Y scales.
			try
			{
				setChartScales(dataCollection, areaSize);
				if (HorizontalScale == null || VerticalScale == null)
				{
					MessageBox.Show(string.Format("There is no appropriate data in the {0} file", fileName)
						, "Load Data");
					return false;
				}
			}
			catch (ArgumentException ex)
			{
				MessageBox.Show(ex.Message);
				return false;
			}

			// PointMarker.
			GeometryDrawing pointMarker = new GeometryDrawing(Brushes.Brown, new Pen(Brushes.Red, 1)
				, new EllipseGeometry(new Point(0, 0), 4, 4));
			pointMarker.Freeze();

			// Create DataViewCollection.
			ObservableCollection<DataView> dataViewCollection = new ObservableCollection<DataView>();
			for (int i = 0; i < dataCollection.Count; ++i)
			{
				ItemData itemData = dataCollection[i];
				ItemDataView dataView = null;
				switch (itemData.GetType().Name)
				{
					case "SampledCurveData`2": // 2D SampledCurveData
						dataView = new SampledCurveDataView()
						{
							ItemData = itemData,
							HorizontalScale = this.HorizontalScale,
							VerticalScale = this.VerticalScale,
							PointMarker = pointMarker
						};
						break;
					case "ScatteredPointsData`2": // 2D ScatteredPoints
						dataView = new ScatteredPointsDataView()
						{
							ItemData = itemData,
							HorizontalScale = this.HorizontalScale,
							VerticalScale = this.VerticalScale,
							PointMarker = pointMarker
						};
						break;
					case "ScatteredPointsData`3": // 3D ScatteredPoints
						// Get Points enumerator.
						PropertyInfo piPoints = itemData.GetType().GetProperty("Points");
						Debug.Assert(piPoints != null, "piPoints != null");
						var points = piPoints.GetValue(itemData, null) as IEnumerable;
						// The "points" should be IEnumerable<Point<>>. Get the Point<> type.
						Type[] argTypes = points.GetType().GetGenericArguments();
						Debug.Assert(argTypes != null, "argTypes != null");
						Debug.Assert(argTypes.Length == 1, "argTypes.Length == 1"
							, "Point enumerator type should have one argument.");
						Type pointType = argTypes[0];
						// Get types of Dimensions.
						argTypes = pointType.GetGenericArguments();
						Debug.Assert(argTypes != null, "argTypes != null");
						Debug.Assert(argTypes.Length == 3, "argTypes.Length == 3"
							, "Point type should have exctlt three arguments.");

						Type zType = argTypes[2];
						object zMin = null, zMax = null;
						List<object> zSeries = null;
						PropertyInfo piZ = pointType.GetProperty("Z");
						Debug.Assert(piZ != null, "piZ != null");
						var thirdDim = from pt in points.Cast<object>() select piZ.GetValue(pt, null);
						if (zType == typeof(object))
							zSeries = thirdDim.ToList();
						else
						{
							zMin = thirdDim.Min();
							zMax = thirdDim.Max();
						}
						Debug.Assert(zType != null && zMin != null && zMax != null, "zType != null && zMin != null && zMax != null");
						Debug.Assert((zType == typeof(object) && zSeries != null) || (zType != typeof(object) && zSeries == null)
							, "(zType == typeof(object) && zSeries != null) || (zType != typeof(object) && zSeries == null");

						// ZScale
						ChartScale zScale = null;
						if (zType == typeof(double))
							zScale = new ChartLinearScale(zMin, zMax
								, areaSize.Width / points.Cast<object>().Count());
						else if (zType == typeof(DateTime))
							zScale = new ChartDateTimeScale(zMin, zMax
								, areaSize.Width / points.Cast<object>().Count());
						else if (zType == typeof(object))
							zScale = new ChartSeriesScale(zSeries, zMin, zMax
								, areaSize.Width / points.Cast<object>().Count());
						else
							throw new ArgumentException("Unsupported z-dimension base type.");

						dataView = new BubblePointsDataView()
						{
							ItemData = itemData,
							HorizontalScale = this.HorizontalScale,
							VerticalScale = this.VerticalScale,
							ZScale = zScale
						};
						break;
				}

				if (dataView != null)
					dataViewCollection.Add(dataView);
			}

			DataContext = dataViewCollection;

			return true;
		}

		/// <summary>
		/// Sets the Chart control X,Y scales.
		/// </summary>
		/// <param name="dataCollection">The data collection.</param>
		/// <param name="areaSize">Expected Chart area size.</param>
		/// <param name="allowedItemDataTypeNames">Item data type names filter. Can be <c>null</c>.</param>
		/// <exception cref="ArgumentException"/>
		void setChartScales(ObservableCollection<ItemData> dataCollection, Size areaSize
			, string[] allowedItemDataTypeNames)
		{
			// Retrieve the data to construct X,Y scales. Both dimensions must have the same type
			// and object-typed dimension must have the matching coordinate points sequences.
			object xMin = null, xMax = null, yMin = null, yMax = null;
			Type xType = null, yType = null;
			List<object> xSeries = null, ySeries = null;
			for (int i = 0; i < dataCollection.Count; ++i)
			{
				ItemData itemData = dataCollection[i];

				// Filter ItemData types.
				if (allowedItemDataTypeNames != null && allowedItemDataTypeNames.Length > 0)
				{
					if (!allowedItemDataTypeNames.Contains(itemData.GetType().Name))
						continue;
				}

				// Get Points enumerator.
				PropertyInfo piPoints = itemData.GetType().GetProperty("Points");
				if (piPoints == null)
					continue; // The item doesn't contain the points.
				var points = piPoints.GetValue(itemData, null) as IEnumerable;
				// The "points" should be IEnumerable<Point<>>. Get the Point<> type.
				Type[] argTypes = points.GetType().GetGenericArguments();
				Debug.Assert(argTypes != null, "argTypes != null");
				Debug.Assert(argTypes.Length == 1, "argTypes.Length == 1"
					, "Point enumerator type should have one argument.");
				Type pointType = argTypes[0];
				// Get types of Dimensions.
				argTypes = pointType.GetGenericArguments();
				Debug.Assert(argTypes != null, "argTypes != null");
				Debug.Assert(argTypes.Length > 1, "argTypes.Length > 1"
					, "Point type should have at least two arguments.");

				// Store and check X,Y dimensions types.
				if (xType == null)
					xType = argTypes[0];
				else if (xType != argTypes[0])
					throw new ArgumentException("Input file contains Point Series with mismatching abscissa base types.");
				if (yType == null)
					yType = argTypes[1];
				else if (yType != argTypes[1])
					throw new ArgumentException("Input file contains Point Series with mismatching ordinate base types.");

				// Abscissas.
				PropertyInfo piX = pointType.GetProperty("X");
				Debug.Assert(piX != null, "piX != null");
				var abscissas = from pt in points.Cast<object>() select piX.GetValue(pt, null);
				if (xType == typeof(object))
				{
					if (xSeries == null)
					{
						xSeries = abscissas.ToList();
						xMin = xSeries[0];
						xMax = xSeries[xSeries.Count - 1];
					}
					else if (!xSeries.SequenceEqual(abscissas))
						throw new ArgumentException("Abscissa Point Series mismatch.");
				}
				else if (xType.GetInterface("IComparable") != null)
				{
					var min = abscissas.Min();
					if (xMin == null)
						xMin = min;
					else if ((min as IComparable).CompareTo(xMin) < 0)
						xMin = min;

					var max = abscissas.Max();
					if (xMax == null)
						xMax = max;
					else if ((max as IComparable).CompareTo(xMax) > 0)
						xMax = max;
				}
				// Ordinates.
				PropertyInfo piY = pointType.GetProperty("Y");
				Debug.Assert(piY != null, "piY != null");
				var ordinates = from pt in points.Cast<object>() select piY.GetValue(pt, null);
				if (yType == typeof(object))
				{
					if (ySeries == null)
					{
						ySeries = ordinates.ToList();
						yMin = ySeries[0];
						yMax = ySeries[ySeries.Count - 1];
					}
					else if (!ySeries.SequenceEqual(ordinates))
						throw new ArgumentException("Ordinates Point Series mismatch.");
				}
				else if (yType.GetInterface("IComparable") != null)
				{
					var min = ordinates.Min();
					if (yMin == null)
						yMin = min;
					else if ((min as IComparable).CompareTo(yMin) < 0)
						yMin = min;

					var max = ordinates.Max();
					if (yMax == null)
						yMax = max;
					else if ((max as IComparable).CompareTo(yMax) > 0)
						yMax = max;
				}
			}
			Debug.Assert(xType != null && xMin != null && xMax != null, "xType != null && xMin != null && xMax != null");
			Debug.Assert((xType == typeof(object) && xSeries != null) || (xType != typeof(object) && xSeries == null)
				, "(xType == typeof(object) && xSeries != null) || (xType != typeof(object) && xSeries == null)");
			Debug.Assert(yType != null && yMin != null && yMax != null, "yType != null && yMin != null && yMax != null");
			Debug.Assert((yType == typeof(object) && ySeries != null) || (yType != typeof(object) && ySeries == null)
				, "(yType == typeof(object) && ySeries != null) || (yType != typeof(object) && ySeries == null)");

			// HorizontalScale
			if (xType == typeof(double))
				HorizontalScale = new ChartLinearScale(xMin, xMax, areaSize.Width);
			else if (xType == typeof(DateTime))
				HorizontalScale = new ChartDateTimeScale(xMin, xMax, areaSize.Width);
			else if (xType == typeof(object))
				HorizontalScale = new ChartSeriesScale(xSeries, xMin, xMax, areaSize.Width);
			else
				throw new ArgumentException("Unsupported abscissa base type.");
			// VerticalScale
			if (yType == typeof(double))
				VerticalScale = new ChartLinearScale(yMax, yMin, areaSize.Height);
			else if (yType == typeof(DateTime))
				VerticalScale = new ChartDateTimeScale(yMax, yMin, areaSize.Height);
			else if (yType == typeof(object))
				VerticalScale = new ChartSeriesScale(ySeries, yMax, yMin, areaSize.Height);
			else
				throw new ArgumentException("Unsupported ordinate base type.");
		}
		/// <summary>
		/// Sets the Chart control X,Y scales.
		/// </summary>
		/// <param name="dataCollection">The data collection.</param>
		/// <param name="areaSize">Expected Chart area size.</param>
		/// <exception cref="ArgumentException"/>
		void setChartScales(ObservableCollection<ItemData> dataCollection, Size areaSize)
		{
			setChartScales(dataCollection, areaSize, null);
		}
		#endregion Load Data

		#region Menu actions handlers
		/// <summary>
		/// Handles the Click event of the MRU menu subitems.
		/// </summary>
		/// <param name="sender">The source of the event.</param>
		/// <param name="e">The <see cref="System.Windows.RoutedEventArgs"/> instance containing the event data.</param>
		private void mru_Click(Object sender, RoutedEventArgs e)
		{
			MenuItem item = sender as MenuItem;
			if (item == null)
				return;
			string fileName = item.Header as string;
			if (!loadCurveData(fileName))
				// Remove from MRU list
				mnuRecentFiles.Items.Remove(item);
		}

		/// <summary>
		/// Handles the Click event of the Exit control.
		/// </summary>
		/// <param name="sender">The source of the event.</param>
		/// <param name="e">The <see cref="System.Windows.RoutedEventArgs"/> instance containing the event data.</param>
		private void Exit_Click(object sender, RoutedEventArgs e)
		{
			Close();
		}
		#endregion Menu actions handlers

		#region Command Handlers
		/// <summary>
		/// Execute <see cref="ApplicationCommands.Open"/> command.
		/// </summary>
		/// <param name="sender">The sender.</param>
		/// <param name="e">The <see cref="System.Windows.Input.ExecutedRoutedEventArgs"/> instance containing the event data.</param>
		private void OpenExecutedCommandHandler(object sender, ExecutedRoutedEventArgs e)
		{
			OpenFileDialog dlg = new OpenFileDialog();
			dlg.Filter = "Text Files (*.txt)|*.txt|XML Files (*.xml)|*.xml|All Files (*.*)|*.*";
			if (dlg.ShowDialog() == true)
			{
				if (loadCurveData(dlg.FileName))
				{
					// Add to MRU list
					foreach (MenuItem item in mnuRecentFiles.Items)
					{
						if (dlg.FileName == item.Header as string)
							return;
					}

					MenuItem newItem = new MenuItem() { Header = dlg.FileName };
					newItem.Click += mru_Click;
					mnuRecentFiles.Items.Add(newItem);
				}
			}
		}

		/// <summary>
		/// Executes <see cref="ApplicationCommands.Properties"/> command.
		/// </summary>
		/// <param name="sender">The sender.</param>
		/// <param name="e">The <see cref="System.Windows.Input.ExecutedRoutedEventArgs"/> instance containing the event data.</param>
		void PropertiesExecutedCommandHandler(object sender, ExecutedRoutedEventArgs e)
		{
			ContextMenu mnu = (ContextMenu)FindResource("chartItemCtxMnu");
			Debug.Assert(mnu != null, "mnu != null");
			ListBoxItem listBoxItem = e.OriginalSource as ListBoxItem;
			Debug.Assert(listBoxItem != null, "listBoxItem != null");
			ItemDataView itemDataView = listBoxItem.DataContext as ItemDataView;
			Debug.Assert(itemDataView != null, "itemDataView != null");

			if (mnu.Tag != null)
			{ // Show Item Properties dialog.
				ItemPropertiesDialog dlg = new ItemPropertiesDialog(itemDataView.Clone() as ItemDataView)
				{ 
					Owner = this,
					CanChangeVerticalScaleType = false,
					CanChangeHorizontalScaleType = false
				};
				if (dlg.ShowDialog() == true)
				{
					if (chart.ItemsSource != null)
					{ // Replace chart.ItemsSource.
						ObservableCollection<DataView> coll = new ObservableCollection<DataView>(
							chart.ItemsSource.Cast<DataView>());
						// Replace ItemDataView changed.
						int index = coll.IndexOf(itemDataView);
						Debug.Assert(index >= 0, "index >= 0");
						coll[index] = dlg.ItemDataView;

						chart.ItemsSource = null;

						foreach (ItemDataView item in coll.OfType<ItemDataView>())
						{
							item.HorizontalScale = dlg.ItemDataView.HorizontalScale;
							item.VerticalScale = dlg.ItemDataView.VerticalScale;
						}

						HorizontalScale = dlg.ItemDataView.HorizontalScale;
						VerticalScale = dlg.ItemDataView.VerticalScale;
						chart.ItemsSource = coll;
					}
					else // chart.ItemsSource is null.
					{ // Replace ItemDataView changed.
						int index = chart.Items.IndexOf(itemDataView);
						Debug.Assert(index >= 0, "index >= 0");
						chart.Items[index] = dlg.ItemDataView;

						foreach (ItemDataView item in chart.Items.OfType<ItemDataView>())
						{
							item.HorizontalScale = dlg.ItemDataView.HorizontalScale;
							item.VerticalScale = dlg.ItemDataView.VerticalScale;
						}

						HorizontalScale = dlg.ItemDataView.HorizontalScale;
						VerticalScale = dlg.ItemDataView.VerticalScale;
					}
				}
				e.Handled = true;
			}
		}

		/// <summary>
		/// Execute <see cref="NavigationCommands.Zoom"/> command.
		/// </summary>
		/// <param name="sender">The sender.</param>
		/// <param name="e">The <see cref="System.Windows.Input.ExecutedRoutedEventArgs"/> instance containing the event data.</param>
		private void ZoomExecutedCommandHandler(object sender, ExecutedRoutedEventArgs e)
		{
			double zoom = 1.2;
			if (e.Parameter != null)
			{
				try
				{
					zoom = Convert.ToDouble(e.Parameter);
				}
				catch (Exception ex)
				{
					Trace.WriteLine("ZoomCommandHandler " + ex.Message);
					return;
				}
			}

			if (zoom == 0.0)
				return;
			if (zoom < 0.0)
				zoom = -1.0 / zoom;

			HorizontalScale.Scale *= zoom;
			VerticalScale.Scale *= zoom;
		}
		#endregion Command Handlers
	}
}
